#import <ifaddrs.h>
#import <arpa/inet.h>

#import "RND2d.h"
#import "RND2dPlayerManager.h"

#if __has_include("React/RCTLog.h")
#import "React/RCTLog.h"
#else
#import <RCTLog.h>
#endif

NSString* const RND2D_ON_COMPLETE = @"d2dOnComplete";
NSString* const RND2D_ON_DATA = @"d2dOnData";

@interface RND2d () {
    NSString* _serverAddr;
    NSMutableDictionary<NSNumber *, D2d *> *_d2ds;
    AVAudioSessionCategory _avAudioSessionCategory;
    AVAudioSessionCategoryOptions _avAudioSessionCategoryOptions;
}
@end

@implementation RND2d

- (dispatch_queue_t)methodQueue
{
    return dispatch_get_main_queue();
}
RCT_EXPORT_MODULE()

RCT_EXPORT_METHOD(init:(nonnull NSNumber *)id
            resolver:(RCTPromiseResolveBlock)resolve
            rejecter:(RCTPromiseRejectBlock)reject)
{
    if(nil == [self findD2d:id]){
        D2d* d2d = [D2d new];
        d2d.delegate = self;
//        [d2d addObserver:self selector:@selector(onComplete:) name:D2D_ON_COMPLETE];
//        [d2d addObserver:self selector:@selector(onData:) name:D2D_ON_DATA];
        [_d2ds setObject:d2d forKey:id];
    }
    resolve(nil);
}

RCT_EXPORT_METHOD(create:(nonnull NSNumber *)id :(NSString *)addr username:(NSString *)username password:(NSString *)password
            resolver:(RCTPromiseResolveBlock)resolve
            rejecter:(RCTPromiseRejectBlock)reject)
{
    _serverAddr = addr;
    int ret = [[self findD2d:id] create:addr :addr :username :password];
    resolve([NSNumber numberWithInt:ret]);
}

RCT_EXPORT_METHOD(getSdp:(nonnull NSNumber *)id :(RCTPromiseResolveBlock)resolve
            rejecter:(RCTPromiseRejectBlock)reject)
{
    NSString * sdp = [[self findD2d:id] getSdp];
    if(nil == _serverAddr){
        sdp = [self resolveSdp:sdp];
    }
    resolve(sdp);
}

RCT_EXPORT_METHOD(getSdpBase64:(nonnull NSNumber *)id :(RCTPromiseResolveBlock)resolve
            rejecter:(RCTPromiseRejectBlock)reject)
{
    NSString * sdp = [[self findD2d:id] getSdp];
    if(nil == _serverAddr){
        sdp = [self resolveSdp:sdp];
    }
    resolve([[sdp dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions: 0]);
}

RCT_EXPORT_METHOD(start:(nonnull NSNumber *)id :(NSString *)sdp :(NSString *)client
        resolver:(RCTPromiseResolveBlock)resolve
        rejecter:(RCTPromiseRejectBlock)reject)
{
    int ret = [[self findD2d:id] start:sdp :client];
    resolve([NSNumber numberWithInt:ret]);
}

RCT_EXPORT_METHOD(startBase64:(nonnull NSNumber *)id :(NSString *)sdp :(NSString *)client
        resolver:(RCTPromiseResolveBlock)resolve
        rejecter:(RCTPromiseRejectBlock)reject)
{
    NSData *sdpData = [[NSData alloc] initWithBase64EncodedString:sdp options:0];
    NSString *remoteSdp = [[NSString alloc] initWithData:sdpData encoding:NSUTF8StringEncoding];
    int ret = [[self findD2d:id] start:remoteSdp :client];
    resolve([NSNumber numberWithInt:ret]);
}

RCT_EXPORT_METHOD(initRtpSocket:(nonnull NSNumber *)id :(NSString *)ip videoPort:(NSInteger)vport audioPort:(NSInteger)aport
            resolver:(RCTPromiseResolveBlock)resolve
            rejecter:(RCTPromiseRejectBlock)reject)
{
    int ret = [[self findD2d:id] initRtpSocket:ip :(int)vport :(int)aport];
    resolve([NSNumber numberWithInt:ret]);
}

RCT_EXPORT_METHOD(initAudio:(nonnull NSNumber *)id :(NSDictionary *) options resolver:(RCTPromiseResolveBlock)resolve
            rejecter:(__unused RCTPromiseRejectBlock)reject) {
    RCTLogInfo(@"init");
    _recordState.mDataFormat.mSampleRate        = options[@"sampleRate"] == nil ? 44100 : [options[@"sampleRate"] doubleValue];
    _recordState.mDataFormat.mBitsPerChannel    = options[@"bitsPerSample"] == nil ? 16 : [options[@"bitsPerSample"] unsignedIntValue];
    _recordState.mDataFormat.mChannelsPerFrame  = options[@"channels"] == nil ? 1 : [options[@"channels"] unsignedIntValue];
    _recordState.mDataFormat.mBytesPerPacket    = (_recordState.mDataFormat.mBitsPerChannel / 8) * _recordState.mDataFormat.mChannelsPerFrame;
    _recordState.mDataFormat.mBytesPerFrame     = _recordState.mDataFormat.mBytesPerPacket;
    _recordState.mDataFormat.mFramesPerPacket   = 1;
    _recordState.mDataFormat.mReserved          = 0;
    _recordState.mDataFormat.mFormatID          = kAudioFormatLinearPCM;
    _recordState.mDataFormat.mFormatFlags       = _recordState.mDataFormat.mBitsPerChannel == 8 ? kLinearPCMFormatFlagIsPacked : (kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked);


    _recordState.bufferByteSize = 2048;
//    _recordState.mSelf = self;
    _recordState.mSelf = [self findD2d:id];
    resolve(0);
}

RCT_EXPORT_METHOD(startAudio:(nonnull NSNumber *)id :(RCTPromiseResolveBlock)resolve
            rejecter:(__unused RCTPromiseRejectBlock)reject) {
    RCTLogInfo(@"start audio");

    // most audio players set session category to "Playback", record won't work in this mode
    // therefore set session category to "Record" before recording
    OSStatus status = 1;
    if (!_recordState.mIsRunning) {
        AVAudioSession* avAudioSession = [AVAudioSession sharedInstance];
        _avAudioSessionCategory = avAudioSession.category;
        _avAudioSessionCategoryOptions = avAudioSession.categoryOptions;
        [avAudioSession setCategory:AVAudioSessionCategoryRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];

        _recordState.mIsRunning = true;

        AudioQueueNewInput(&_recordState.mDataFormat, HandleInputBuffer, &_recordState, NULL, NULL, 0, &_recordState.mQueue);
        for (int i = 0; i < kNumberBuffers; i++) {
            AudioQueueAllocateBuffer(_recordState.mQueue, _recordState.bufferByteSize, &_recordState.mBuffers[i]);
            AudioQueueEnqueueBuffer(_recordState.mQueue, _recordState.mBuffers[i], 0, NULL);
        }
        OSStatus status = AudioQueueStart(_recordState.mQueue, NULL);
    }
    resolve([NSNumber numberWithInt:status]);
}

RCT_EXPORT_METHOD(stopAudio:(nonnull NSNumber *)id :(RCTPromiseResolveBlock)resolve
            rejecter:(__unused RCTPromiseRejectBlock)reject) {
    RCTLogInfo(@"stop audio");
    OSStatus status = 1;
    if (_recordState.mIsRunning) {
        _recordState.mIsRunning = false;
        AudioQueueDispose(_recordState.mQueue, true);
        status = AudioQueueStop(_recordState.mQueue, true);
        [[AVAudioSession sharedInstance] setCategory:_avAudioSessionCategory withOptions:_avAudioSessionCategoryOptions error:nil];
    }
    resolve([NSNumber numberWithInt:status]);
}

RCT_EXPORT_METHOD(destroy:(nonnull NSNumber *)id :(RCTPromiseResolveBlock)resolve
            rejecter:(__unused RCTPromiseRejectBlock)reject)
{
    [[self findD2d:id] destroy];
    resolve(NULL);
}

RCT_EXPORT_METHOD(setTimeout:(nonnull NSNumber *)id :(nonnull NSNumber *)timeout :(RCTPromiseResolveBlock)resolve
        rejecter:(__unused RCTPromiseRejectBlock)reject)
{
    [[self findD2d:id] setTimeout:[timeout integerValue]];
    resolve(NULL);
}

RCT_EXPORT_METHOD(setPeerSourceReportInterval:(nonnull NSNumber *)id :(nonnull NSNumber *)interval :(RCTPromiseResolveBlock)resolve
        rejecter:(__unused RCTPromiseRejectBlock)reject)
{
    [[self findD2d:id] setPeerSourceReportInterval:[interval integerValue]];
    resolve(NULL);
}

RCT_EXPORT_METHOD(bindPlayer:(nonnull NSNumber *)id :(RCTPromiseResolveBlock)resolve
        rejecter:(__unused RCTPromiseRejectBlock)reject)
{
    [self findD2d:id].mediaDelegate = RND2dPlayerManager.rnd2dPlayer.d2dPlayer;
    resolve(NULL);
}

void HandleInputBuffer(void *inUserData,
        AudioQueueRef inAQ,
        AudioQueueBufferRef inBuffer,
        const AudioTimeStamp *inStartTime,
        UInt32 inNumPackets,
        const AudioStreamPacketDescription *inPacketDesc) {
    AQRecordState* pRecordState = (AQRecordState *)inUserData;

    if (!pRecordState->mIsRunning) {
        return;
    }

    short *samples = (short *) inBuffer->mAudioData;
    long nsamples = inBuffer->mAudioDataByteSize;
    NSData *data = [NSData dataWithBytes:samples length:nsamples];
    dispatch_async(dispatch_get_main_queue(), ^{
        [((D2d*)pRecordState->mSelf) sendPCM:data :nsamples];
    });
    AudioQueueEnqueueBuffer(pRecordState->mQueue, inBuffer, 0, NULL);
}

- (instancetype) init {
    self = [super init];
    _d2ds = [NSMutableDictionary new];
    /*_d2d = [D2d new];

    [_d2d addObserver:self selector:@selector(onComplete:) name:D2D_ON_COMPLETE];
    [_d2d addObserver:self selector:@selector(onData:) name:D2D_ON_DATA];*/

    return self;
}

- (void)onComplete:(D2d*)d2d :(NSInteger)op :(NSInteger)status {
//- (void) onComplete :(D2d*)d2d :(NSNotification*)notification {
    //int op = [notification.userInfo[D2D_COMPLETE_OP] intValue];
    //int status = [notification.userInfo[D2D_COMPLETE_STATUS] intValue];
//    NSArray* keys = [_d2ds allKeysForObject:notification.object];
//    [self sendEventWithName:getEventName(RND2D_ON_COMPLETE,keys[0]) body:notification.userInfo];
    NSArray* keys = [_d2ds allKeysForObject:d2d];
    [self sendEventWithName:getEventName(RND2D_ON_COMPLETE,keys[0]) body:@{ D2D_COMPLETE_OP:@(op), D2D_COMPLETE_STATUS:@(status) }];
}

- (void)onData:(D2d*)d2d :(NSInteger)op :(NSString*)addr :(NSInteger)candType {
//- (void) onData :(D2d*)d2d :(NSNotification*)notification {
//    NSArray* keys = [_d2ds allKeysForObject:notification.object];
//    [self sendEventWithName:getEventName(RND2D_ON_DATA,keys[0]) body:notification.userInfo];
    NSArray* keys = [_d2ds allKeysForObject:d2d];
    [self sendEventWithName:getEventName(RND2D_ON_DATA,keys[0]) body:@{ D2D_DATA_OP:@(op), D2D_DATA_ADDR:addr, D2D_DATA_CAND_TYPE:@(candType) }];
}

- (NSDictionary<NSString *, NSString *> *) constantsToExport {
    NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:RND2D_ON_COMPLETE,@"D2D_ON_COMPLETE",RND2D_ON_DATA,@"D2D_ON_DATA",nil];
//    [dict setValue:D2D_ON_COMPLETE forKey:D2D_ON_COMPLETE];
//    [dict setValue:D2D_ON_DATA forKey:D2D_ON_DATA];
    return dict;
}

- (NSArray<NSString *> *) supportedEvents {
//    return @[RND2D_ON_COMPLETE,RND2D_ON_DATA];
    NSMutableArray *events = nil;
    if(nil != _d2ds){
        events = [NSMutableArray arrayWithCapacity:[_d2ds count]*2];
        NSEnumerator *keyEnum = [_d2ds keyEnumerator];
        NSNumber* key;
        while ((key = [keyEnum nextObject])) {
            [events addObject:getEventName(RND2D_ON_COMPLETE,key)];
            [events addObject:getEventName(RND2D_ON_DATA,key)];
        }

    }else{
        events = [NSMutableArray arrayWithArray:@[RND2D_ON_COMPLETE,RND2D_ON_DATA]];
    }
    return events;
}

NSString* getEventName(NSString* eventName, NSNumber * id){
    return [NSString stringWithFormat:@"%@-%@", eventName, id];
}

- (void) dealloc {
    RCTLogInfo(@"dealloc");

    @try{
        AudioQueueDispose(_recordState.mQueue, true);
    }@catch(NSException *exception){
        NSLog(@"%@", exception);
    }
    _d2ds = nil;
    /*[_d2d removeObserver:self name:D2D_ON_COMPLETE];
    [_d2d removeObserver:self name:D2D_ON_DATA];
    [_d2d destroy];
    _d2d = nil;*/

}

- (NSString *) resolveSdp:(NSString*)sdp{
    NSString* sep = @"\n";
    NSString* prefix = @"a=candidate:";
    NSString* wifiIP = [self getWifiIPAddress];
    NSArray *array = [sdp componentsSeparatedByString:sep];
    NSMutableArray * sdpArr = [[NSMutableArray alloc] initWithCapacity:[array count]];
    for (NSString *token in array) {
        if([token hasPrefix:prefix] && ![token containsString:wifiIP]){
            continue;
        }else{
            [sdpArr addObject:token];
        }
    }
    return [sdpArr componentsJoinedByString:sep];
}

- (NSString *) getWifiIPAddress
{
    NSString *address = @"error";
    struct ifaddrs *interfaces = NULL;
    struct ifaddrs *temp_addr = NULL;
    int success = 0;
    // retrieve the current interfaces - returns 0 on success
    success = getifaddrs(&interfaces);
    if (success == 0) {
        // Loop through linked list of interfaces
        temp_addr = interfaces;
        while(temp_addr != NULL) {
            if(temp_addr->ifa_addr->sa_family == AF_INET) {
                // Check if interface is en0 which is the wifi connection on the iPhone
                if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
                    // Get NSString from C String
                    address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
                }
            }
            temp_addr = temp_addr->ifa_next;
        }
    }
    // Free memory
    freeifaddrs(interfaces);
    return address;
}

- (D2d*) findD2d:(NSNumber*)id
{
    D2d* d2d = [_d2ds objectForKey:id];
    return d2d;
}
@end